﻿#region Using and Instructions
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.IO;
using System.Runtime.Serialization;


#pragma warning disable 1587

/// Microsoft.Web.Media.TransformManager
/// 
/// Reference:
///    c:\Program Files\IIS\Transform Manager\Microsoft.Web.Media.TransformManager.SDK.dll
///    c:\Program Files\IIS\Transform Manager\Microsoft.Web.Media.TransformManager.Common.dll
///
/// Download from: http://www.microsoft.com/en-us/download/details.aspx?id=29889
///   Or WebPI:  http://go.microsoft.com/fwlink/?linkid=255386  
/// 
/// Requires: IIS Media Services 4.1, http://go.microsoft.com/?linkid=9789095
/// 
/// Support:
///  IIS Forums: http://forums.iis.net/1145.aspx
///  Microsoft Support via support ticket or MSFT Premier account.
///  
using Microsoft.Web.Media.TransformManager;


/// Microsoft.WindowsAzure.MediaServices.Client
/// 
/// References:
///  Download via NuGet: install-package WindowsAzure.MediaServices -Version 2.0.0.5
///
/// Support:
///  Windows Azure Media Services Forums: http://social.msdn.microsoft.com/Forums/da-dk/MediaServices/threads
///  
using Microsoft.WindowsAzure.MediaServices.Client;


/// Signing your assembly:
///
///  You should sign your assembly using a strong name key file (.snk)
///  
///  See MSDN: http://msdn.microsoft.com/en-us/library/ms247123(v=vs.100).aspx
/// 
///  Set/Create the .snk file in the project properties in the Signing tab.
///   
///   In CreateAssetTask.xml, change:
///      <type>Codeplex.TransformManager.WAMS.CreateAssetTask, Codeplex.TransformManager.WAMS.CreateAssetTask, Version=1.0.0.0, Culture=neutral</type>
///   To:
///      <type>Codeplex.TransformManager.WAMS.CreateAssetTask, Codeplex.TransformManager.WAMS.CreateAssetTask, Version=1.0.0.0, Culture=neutral, PublicKeyToken=__Your_PublicKey_Token__</type>
///


/// Post-Build / Deployment Steps:
/// 
///  copy /Y $(TargetPath) "C:\Program Files\IIS\Transform Manager\"
///  copy /Y $(TargetDir)Microsoft.WindowsAzure.MediaServices.Client.dll "C:\Program Files\IIS\Transform Manager\"
///  copy /Y $(TargetDir)Microsoft.WindowsAzure.StorageClient.dll "C:\Program Files\IIS\Transform Manager\"
///  copy /Y $(TargetDir)CreateAssetTask.xml "C:\ProgramData\Microsoft\IIS\Transform Manager\Configuration\Task Definitions\"
///  
/// Installing in Transform Mananger:
///
///        Copy files as indicated above.
/// 
///        This task is executed by TaskEngine.exe.  
///        As such, all runtime binding redirects must be in TaskEngine.exe.config.
///        Specifically, add the following to "C:\Program Files\IIS\Transform Manager\TaskEngine.exe.config"
///        <runtime>
///         <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
///           <dependentAssembly>
///             <assemblyIdentity name="Microsoft.Practices.TransientFaultHandling.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
///            <bindingRedirect oldVersion="0.0.0.0-5.1.1209.0" newVersion="5.1.1209.0" />
///           </dependentAssembly>
///          <dependentAssembly>
///             <assemblyIdentity name="Microsoft.WindowsAzure.StorageClient" publicKeyToken="31bf3856ad364e35" culture="neutral" />
///             <bindingRedirect oldVersion="0.0.0.0-1.7.0.0" newVersion="1.7.0.0" />
///           </dependentAssembly>
///           <dependentAssembly>
///             <assemblyIdentity name="Microsoft.Data.Services.Client" publicKeyToken="31bf3856ad364e35" culture="neutral" />
///            <bindingRedirect oldVersion="0.0.0.0-5.1.0.0" newVersion="5.1.0.0" />
///           </dependentAssembly>
///         </assemblyBinding>
///        </runtime>
///        Updated versions of the above will be in this project's app.config and are managed by NuGet package updates.
/// 
///        Restart TM Service via user interface
///        Create a template for the new task.
///        Set your confirguration properties for the template.
///        
///        Create a watch-folder.  
///        You can choose *.ism as the trigger file. 
///        Enable and start-up the watch folder.
///
///        Drop some smooth assets into the watch folder.
///
///        The Asset.Id and PrimaryFile.Uri are written to both the TM log file and a .json file.
///        Read the .json file your player app.
///        
///        If you used a .ism as the watch-folder trigger and set primaryFileMask==[InstanceFile],
///        then "PrimaryFileUri/manifest" is the path you need to stream back your smooth content.
///



/// Using this project as a template for your own TM tasks:
/// 
///   Create a .Net Framework 4 class library 
///   Make sure targets full framework, not the "Client Profile"
///   Add the references for Transform Manager.
///   Add the using Microsoft.Web.Media.TransformManager; 
///   Make your class derive from  : Microsoft.Web.Media.TransformManager.ITask
///   Right-click that and 'Implement Interface'
///   Copy and re-use some of the base Transform Manager variables and checks.
///   Copy and change values in the task.xml, specifically, you must change
///     - the id GUID 
///     - the task properties namespace and prefix
///     - the taskcode to point to your class name and dll name
/// 


/// Overriding properties of this task when submitting a smil file to TM:
/// 
///   When you use a .smil file to submit your assets to TM, you can override 
///   any of the properties that you set in the TM GUI on an per-asset basis.
///   To do this, add a section to the head of the smil file with the following   
/// 
///      <?xml version="1.0" encoding="utf-8"?>
///      <smil xmlns="http://www.w3.org/2001/SMIL20/Language">
///        <head>
///          <metadata id="meta-rdf">
///            <rdf:RDF xmlns:iisms="http://schemas.microsoft.com/iis/media/v4/TM#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" >
///              <iisms:task rdf:about="guid:92c19155-c012-478d-bf61-47c937616b2a" xmlns:createAsset="http://schemas.microsoft.com/iis/media/CreateAsset#">
///                <iisms:id>92c19155-c012-478d-bf61-47c937616b2a</iisms:id>
///                <createAsset:alternateId>My CMS GUID for this asset</createAsset:alternateId>
///              </iisms:task>
///            </rdf:RDF>
///          </metadata>
///        </head>
///        <body>
///          <switch>
///            <video src="my.mp4" />
///            <audio src="my.mp4" />
///          </switch>
///        </body>
///      </smil>
/// 
///   For example, you could use the above in the following use case:
///     If your CMS system, which is submitting jobs to TM, is already authoring the smil file.
///     Add the a section above to to this smil and set your CMS GUID for the asset,
///     then you can find the asset easily in WAMS using the function in this sample:
///     
///        IAsset theAsset = GetAssetByAlternateId("My CMS GUID for this asset");
///        if(theAsset != null)
///        {
///          //Do something with the asset.
///        }
/// 


/// 
/// Chaining tasks:
///     In Transform Manager, you can chain tasks to support larger workflows.
///     This task can be chained to itself, if you do this, it will exhibit a special behavior.
/// Use case: 
///     Assume you want to upload the same asset to one or more datacenters to provide origin services replication and failover.
///     Create a first instance of the task to upload and create a locator, in say, data center EAST_US.
///     Chain a second instance of the task to upload and create a locator in WEST_US.
///     The second instance will read the .json output of the first instance to see what locator Id was used.
///     It will re-use the locator id by calling the REST API directly, passing the first locator Id in the POST request to the second datacenter.
///     This will create a locator with the same path as the first datacenter.
///     By doing this, and setting up a load-balancer or CDN to source from either datacenter, you are creating a failover for your asset across datacenters.
///       http://yourEastUsOrigin/locatorguid/yourfile.ism/manifest
///       http://yourWestUsOrigin/locatorguid/yourfile.ism/manifest
/// 
///     This redundancy, of course, drives up both storage and origin-services costs.
/// 
/// Note that TM has an issue when the same task is repeated in a single template:
///   First add the tasks, then save and close the template.
///   Now reopen the template and make modifications to each task:
///    Add your credentials to each task.
///    Use an output folder on the first task, say "firstUpload".
///    Use the same folder as the input for the second task.
///    The second task does not need a specific output folder.
/// 

#endregion

#region Todo List
///
///  Look into overriding the AssetCreationOptions.  Particularly if SmoothProtect is a prior task.
///
#pragma warning restore 1587
#endregion


namespace Codeplex.TransformManager.WAMS
{


    public class CreateAssetTask : Microsoft.Web.Media.TransformManager.ITask
    {
        // 
        // TM Task variables.
        //
        private Boolean _initialized = false;
        private IJobMetadata _transformMetadata = null;
        private ITaskStatus _transformTaskStatus = null;
        private ILogger _transformLogger = null;

        //
        // Task specific:
        //
        //Properties:
        private string _alternateId = string.Empty;
        private static string _jobInstanceIdToken = "[UseJobInstanceId]";
        private double _accessPolicyDurationInDays = 365;
        private double _locatorStartTimeInMinutes = -5;
        private static string _instanceFileToken = "[UseInstanceFile]";
        private string _primaryFileMask = _instanceFileToken;
        private string _apiServer = string.Empty;
        private string _scope = string.Empty;
        private string _acsBaseAddress = string.Empty;
        private string _wamsAccountName = "EnterYourAccoutName"; //Set to task xml defaults for parameter validation logic
        private string _wamsAccountKey = "EnterYourAccoutKey";   //Set to task xml defaults for parameter validation logic
        //Global Variables:
        private CloudMediaContext _context = null;
        private AssetMetadata _AssetMetadata = new AssetMetadata();
        private const int ProgressValuePriorToUploading = 15;
        private const double ProgressPercentageAttributedToUploading = 0.75;

        public void Dispose()
        {
        }

        #region Initialization

        public void Initialize(ITaskStatus status, IJobMetadata metadata, ILogger logger)
        {
            //
            //  Validate the input parameters.
            //
            if (status == null)
            {
                //
                //  Throw argument null exception  
                //
                throw new ArgumentNullException("status");
            }

            if (metadata == null)
            {
                //
                //  Throw argument null exception  
                //
                throw new ArgumentNullException("metadata");
            }

            if (logger == null)
            {
                //
                //  Throw argument null exception  
                //
                throw new ArgumentNullException("logger");
            }


            //
            //  Assign the local member variables.
            //
            _transformTaskStatus = status;
            _transformMetadata = metadata;
            _transformLogger = logger;

            _transformLogger.WriteLine(LogLevel.Information, "Initializing CreateAssetTask");


            //
            //  Get the task-specific properties from metadata, set local values, or their defaults and normalize them if required.
            //
            GetTaskSpecificProperties();

            //
            // Confirm initialization
            //
            _initialized = true;
        }


        /// <summary>
        /// Get the task-specific properties from metadata, set local values, or their defaults and normalize them if required.
        /// </summary>
        private void GetTaskSpecificProperties()
        {
            XNamespace taskNamespace = "http://schemas.microsoft.com/iis/media/CreateAsset#";
            ManifestProperty alternateId = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "alternateId");
            ManifestProperty accessPolicyDurationInDays = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "accessPolicyDurationInDays");
            ManifestProperty locatorStartTimeInMinutes = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "locatorStartTimeInMinutes");
            ManifestProperty primaryFileMask = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "primaryFileMask");
            ManifestProperty apiServer = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "apiServer");
            ManifestProperty scope = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "scope");
            ManifestProperty acsBaseAddress = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "acsBaseAddress");
            ManifestProperty wamsAccountName = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "wamsAccountName");
            ManifestProperty wamsAccountKey = (ManifestProperty)_transformMetadata.GetProperty(taskNamespace + "wamsAccountKey");


            //
            //  Set local values from properties, or their defaults, normalize them if required
            //
            if (_alternateId == null) //Can be empty, should not be missing (null)
            {
                throw new ArgumentNullException("alternateId");
            }
            else
            {
                _alternateId = alternateId.Value;
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Using alternateId={0}", _alternateId));
            }

            if (accessPolicyDurationInDays == null || String.IsNullOrEmpty(accessPolicyDurationInDays.Value))
            {
                throw new ArgumentNullException("accessPolicyDurationInDays");
            }
            else
            {
                if (!Double.TryParse(accessPolicyDurationInDays.Value, out _accessPolicyDurationInDays))
                {
                    throw new ArgumentException("Unable to parse the value of the task property accessPolicyDurationInDays as a double: {0}", accessPolicyDurationInDays.Value);
                }
                else
                {
                    if (_accessPolicyDurationInDays < 0.0)
                    {
                        throw new ArgumentException("accessPolicyDurationInDays must be greater than or equal to zero: {0}", accessPolicyDurationInDays.Value);
                    }
                    _transformLogger.WriteLine(LogLevel.Information, String.Format("Using accessPolicyDurationInDays={0}", _accessPolicyDurationInDays));
                }
            }

            if (locatorStartTimeInMinutes == null || String.IsNullOrEmpty(locatorStartTimeInMinutes.Value))
            {
                throw new ArgumentNullException("locatorStartTimeInMinutes");
            }
            else
            {
                if (!Double.TryParse(locatorStartTimeInMinutes.Value, out _locatorStartTimeInMinutes))
                {
                    throw new ArgumentException("Unable to parse the value of the task property locatorStartTimeInMinutes as a double: {0}", locatorStartTimeInMinutes.Value);
                }
                else
                {
                    _transformLogger.WriteLine(LogLevel.Information, String.Format("Using locatorStartTimeInMinutes={0}", _locatorStartTimeInMinutes));
                }
            }

            if (primaryFileMask == null || String.IsNullOrEmpty(primaryFileMask.Value))
            {
                throw new ArgumentNullException("primaryFileMask");
            }
            else
            {
                _primaryFileMask = primaryFileMask.Value;
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Using primaryFileMask={0}", _primaryFileMask));
            }

            if (_apiServer == null) //Can be empty, should not be missing (null)
            {
                throw new ArgumentNullException("apiServer");
            }
            else
            {
                _apiServer = apiServer.Value;
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Using apiServer={0}", _apiServer));
            }

            if (_scope == null) //Can be empty, should not be missing (null)
            {
                throw new ArgumentNullException("scope");
            }
            else
            {
                _scope = scope.Value;
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Using scope={0}", _scope));
            }

            if (_acsBaseAddress == null) //Can be empty, should not be missing (null)
            {
                throw new ArgumentNullException("acsBaseAddress");
            }
            else
            {
                _acsBaseAddress = acsBaseAddress.Value;
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Using acsBaseAddress={0}", _acsBaseAddress));
            }

            if (wamsAccountName == null || String.IsNullOrEmpty(wamsAccountName.Value))
            {
                throw new ArgumentNullException("wamsAccountName");
            }
            else if (wamsAccountName.Value == _wamsAccountName)
            {
                throw new ArgumentException("Please ensure to set the wamsAccountName parameter in the task properties of your Job Template.  It is still set to the default value.");
            }
            else
            {
                _wamsAccountName = wamsAccountName.Value;
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Using wamsAccountName={0}", _wamsAccountName));
            }

            if (wamsAccountKey == null || String.IsNullOrEmpty(wamsAccountKey.Value))
            {
                throw new ArgumentNullException("wamsAccountKey");
            }
            else if (wamsAccountKey.Value == _wamsAccountKey)
            {
                throw new ArgumentException("Please ensure to set the wamsAccountKey parameter in the task properties of your Job Template.  It is still set to the default value.");
            }
            else
            {
                _wamsAccountKey = wamsAccountKey.Value; //Don't log this!
            }
        }

        #endregion //Initialization

        public void Start()
        {
            if (!_initialized)
            {
                //
                //  Called before initialize.
                //
                throw new TypeInitializationException("Codeplex.TransformManager.WAMS.CreateAssetTask", null);
            }

            _transformLogger.WriteLine(LogLevel.Information, "Starting CreateAssetTask");

            //
            // Gets the files in the input directory, excluding the Transform Manager job .smil file.
            //
            List<FileInfo> inputDirectoryFiles = GetFileInfoListFromInputDirectory();
            _transformTaskStatus.UpdateStatus(5, JobStatus.Running, null);

            //
            // Looks for a json file, in case this task was chained with itself.
            // Re-use the locator Id to duplicate it across data-centers.
            //
            List<FileInfo> filesToUpload = CheckForJsonFile(inputDirectoryFiles);


            //
            // Uses the _primaryFileMask to find the primary file.
            //            
            _AssetMetadata.PrimaryFile = FindPrimaryFile(filesToUpload);
            _transformTaskStatus.UpdateStatus(10, JobStatus.Running, null);


            //
            // Windows Azure Media Services Specific:
            //

            // Gets the CloudMediaContext set up using the credentials supplied in the task.
            //
            GetContext();
            _transformTaskStatus.UpdateStatus((int)ProgressValuePriorToUploading, JobStatus.Running, null);

            //
            // Create an Asset on WAMS.  Sets _AssetMetadata.AssetId.
            //
            CreateAsset(filesToUpload);
            _transformTaskStatus.UpdateStatus(90, JobStatus.Running, null);

            //
            // Gets a VOD Locator for the Asset.  Sets the AssetMetadata.PrimaryFileUri.
            //
            CreateLocator();
            _transformTaskStatus.UpdateStatus(95, JobStatus.Running, null);

            //
            // Serialize _AssetMetadata to json and write a file in the output directory.
            //
            SaveAssetMetadataAsJson();
            _transformTaskStatus.UpdateStatus(97, JobStatus.Running, null);

            //
            // Copy all input files to the output directory
            //
            CopyToOutputDirectory(inputDirectoryFiles);
            _transformTaskStatus.UpdateStatus(100, JobStatus.Finished, null);


            _transformLogger.WriteLine(LogLevel.Information, "Ending CreateAssetTask");
        }

        #region Runtime functions

        /// <summary>
        /// Gets the files in the input directory, excluding the Transform Manager job .smil file.
        /// </summary>
        /// <returns>List of FileInfo to uplaod.</returns>
        private List<FileInfo> GetFileInfoListFromInputDirectory()
        {
            string localDirectory = Path.Combine(Environment.CurrentDirectory, _transformMetadata.InputFolder);
            DirectoryInfo di = new DirectoryInfo(localDirectory);
            List<FileInfo> fileInfos = di.GetFiles().ToList();
            if (fileInfos.Count == 0)
            {
                throw new FileNotFoundException("No files were found in target local directory");
            }

            //
            // Remove the TM jobId.smil file from the upload list
            //
            String jobId = String.Empty;
            try
            {
                //This could easily throw if TM changes it's manifest format:
                jobId = _transformMetadata.Manifest.Element(IisNamespaces.Smil20Language + "head").
                        Element(IisNamespaces.Smil20Language + "metadata").
                        Element(IisNamespaces.Rdf + "RDF").
                        Element(IisNamespaces.MediaServices + "job").
                        Element(IisNamespaces.MediaServices + "instanceId").Value;
                if (!String.IsNullOrEmpty(jobId))
                {
                    String jobSmil = Path.Combine(Environment.CurrentDirectory, jobId + ".smil");
                    if (File.Exists(jobSmil))
                    {
                        var smil = fileInfos.FirstOrDefault(f => f.FullName.ToLower() == jobSmil.ToLower());
                        if (smil != null)
                            fileInfos.Remove(smil);
                    }
                }
            }
            catch (Exception e)
            {
                //Fail gracefully, non-critial exception:
                _transformLogger.WriteLine(LogLevel.Warning, String.Format("Unable to remove TM jobId.smil file from upload list.  Exception:{0}", e.Message));
            }

            //If we were able to find the job instanceId, and the user has requested to use it as the Asset.AlternateId, then set it here.
            if (!string.IsNullOrEmpty(jobId) && _alternateId.ToLower() == _jobInstanceIdToken.ToLower())
            {
                _alternateId = jobId;
            }

            return fileInfos;
        }



        /// <summary>
        /// Looks for a json file, in case this task was chained with itself.
        /// Sets the _AssetMetadata.LocatorId to the value in the json file, this enables locator.Id re-use, which supports data-center redundancy.
        /// </summary>
        /// <param name="inputDirectoryFiles"></param>
        /// <returns>The list of files to upload.  We remove the json files we find from the upload list, as they have no value in the cloud.</returns>
        private List<FileInfo> CheckForJsonFile(List<FileInfo> inputDirectoryFiles)
        {
            List<FileInfo> filesToUpload = inputDirectoryFiles;

            //We are only expecting one, but will parse them all, using the last parsable one to se the LocatorId
            var jsonFiles = inputDirectoryFiles.Where(f => f.Extension.ToLower() == ".json").OrderBy(f => f.Name).ToList();
            foreach (var jsonFile in jsonFiles)
            {
                try
                {
                    var jsonText = File.ReadAllText(jsonFile.FullName);
                    var previousMetadata = AssetMetadata.FromJson(jsonText);
                    //Re-use the LocatorId to provide cross-data-center streaming redundancy:
                    _AssetMetadata.LocatorId = previousMetadata.LocatorId;

                    //Remove the file from the upload list:
                    filesToUpload.Remove(jsonFile);
                }
                catch (Exception e)
                {
                    //Fail gracefully, non-critial exception:
                    _transformLogger.WriteLine(LogLevel.Warning,
                                               String.Format(String.Format("Unable to parse json file {0}  Exception:{1}",
                                                                           jsonFile.FullName, e.Message)));
                }
            }

            return filesToUpload;
        }



        /// <summary>
        /// Uses the _primaryFileMask to find the primary file.
        /// </summary>
        /// <param name="fileInfos">List of files to search from.</param>
        /// <returns>Primary file according to primaryFileMask property.</returns>
        private string FindPrimaryFile(List<FileInfo> fileInfos)
        {

            string primaryFile = string.Empty;

            //
            // Using the instance file name
            //
            if (_primaryFileMask.ToLower() == _instanceFileToken.ToLower())
            {
                //If it is supposed to be the instance file name, then check if it is still in the directory
                var fileCheck = from file in fileInfos
                                where file.Name.ToLower() == _transformMetadata.InstanceFileName.ToLower()
                                select file.Name;
                primaryFile = fileCheck.FirstOrDefault(); //This will be null if there is no match.
                if (String.IsNullOrEmpty(primaryFile))
                {
                    throw new FileNotFoundException("Could not find the instance file name in the input directory.  Perhaps it was not copied during a previous task.");
                }
                //We're done
                return primaryFile;
            }


            //
            // Using the file mask
            //
            //We need to jump through some hoops to make sure we get it right.
            FileInfo[] primaryFiles = fileInfos[0].Directory.GetFiles(_primaryFileMask);

            if (primaryFiles == null || primaryFiles.Count() == 0)
            {
                throw new FileNotFoundException("Could not find the primary file using the primary file mask.");
            }

            //TODO: Consider rework, perhaps check that the extention is only three chars.  
            if (primaryFiles.Count() > 1) //Need to disambiguate as best as we can
            {
                //Because three letter extension search patterns return all matches of 4, 5, 6, ... lettered extensions, we will run into problems here.
                // Particularly because it will be typical to use *.ism which will return both *.ism and *.ismc.
                // So we will look for this case and try to find an extension which matches the pattern
                /// <see cref="http://msdn.microsoft.com/en-us/library/8he88b63(v=vs.100).aspx"/>
                var frag = _primaryFileMask.Split('.');
                if (frag.Count() == 1) //no period in the search pattern, too many files, we don't know what to do.
                    throw new FileNotFoundException("More than one file was found using the primary file mask.  This task cannot disambiguate between these, use a more restrictive file mask.");
                string ext = "." + frag.Last();
                if (ext.Contains('*') || ext.Contains('?')) //extension has wildcards, we don't know what to do.
                    throw new FileNotFoundException("More than one file was found using the primary file mask.  This task cannot disambiguate between these, use a more restrictive file mask.");

                //TODO: Also check that there is only one file: right now, the logic will pick the last one in the list.
                foreach (var file in primaryFiles)
                {
                    if (file.Extension.ToLower() == ext.ToLower())
                    {
                        primaryFile = file.Name;
                    }
                }
            }
            else
            {
                primaryFile = primaryFiles[0].Name;
            }

            if (string.IsNullOrEmpty(primaryFile))
                throw new FileNotFoundException("Primary file not found.");

            return primaryFile;
        }



        /// <summary>
        /// Gets the CloudMediaContext set up using the credentials supplied in the task.
        /// </summary>
        /// <see cref="http://msdn.microsoft.com/en-us/library/jj125782.aspx"/>
        private void GetContext()
        {
            if (_context == null)
            {
                //
                // Get the context for accessing WAMS
                //
                if (!String.IsNullOrEmpty(_scope) && !String.IsNullOrEmpty(_acsBaseAddress))
                {
                    _context = new CloudMediaContext(new Uri(_apiServer), _wamsAccountName, _wamsAccountKey, _scope, _acsBaseAddress);
                }
                else if (!String.IsNullOrEmpty(_apiServer))
                {
                    _context = new CloudMediaContext(new Uri(_apiServer), _wamsAccountName, _wamsAccountKey);
                }
                else
                {
                    _context = new CloudMediaContext(_wamsAccountName, _wamsAccountKey);
                }
                if (_context == null)
                {
                    throw new ApplicationException("Could not initialize a connection to the Windows Azure Media Serivces");
                }
            }
        }



        /// <summary>
        /// Create an Asset on WAMS.  Sets _AssetMetadata.AssetId.
        /// </summary>
        /// <param name="fileInfos"></param>
        /// <returns></returns>
        private bool CreateAsset(List<FileInfo> fileInfos)
        {
            //
            // Create an asset using the file list and primary file.
            //
            var files = from eachFile in fileInfos
                        select eachFile.FullName;

            IAsset asset = _context.Assets.Create(_AssetMetadata.PrimaryFile, AssetCreationOptions.None);

            UploadFiles(asset, files, _AssetMetadata.PrimaryFile);

            //Set the global metadata object's AssetId value.
            _AssetMetadata.AssetId = asset.Id;


            //Update the metadata in WAMS to include the alternateId passed as a parameter
            if (!string.IsNullOrEmpty(_alternateId))
            {
                //Get an updated reference of the Asset
                asset = GetAssetById(asset.Id);

                //Set the AlternateId
                asset.AlternateId = _alternateId;

                //Save the information in WAMS
                asset.Update();

                //Set the global metadata object's AssetId value.
                _AssetMetadata.AlternateId = _alternateId;
            }

            _transformLogger.WriteLine(LogLevel.Information, "Asset.Id= " + _AssetMetadata.AssetId);

            return true;
        }

        private void UploadFiles(IAsset asset, IEnumerable<string> files, string primaryFileName)
        {
            _transformLogger.WriteLine(LogLevel.Information, "Starting uploads.");

            //Unfortuantely, the next line does not provide progress, so we cannot report it back to the Transform Manager service.
            var accessPolicy = _context.AccessPolicies.Create(primaryFileName, TimeSpan.FromHours(12),
                                                              AccessPermissions.Write | AccessPermissions.List);
            var locator = _context.Locators.CreateLocator(LocatorType.Sas, asset, accessPolicy);
            var blobTransferClient = new BlobTransferClient();

            var uploadTasks = new List<Task>();
            foreach (var filePath in files)
            {
                IAssetFile assetFile = asset.AssetFiles.Create(Path.GetFileName(filePath));
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Created assetFile {0}", assetFile.Name));

                if (String.Compare(assetFile.Name, primaryFileName, true) == 0)
                {
                    assetFile.IsPrimary = true;
                    uploadTasks.Add(assetFile.UpdateAsync());
                }

                uploadTasks.Add(assetFile.UploadAsync(filePath, blobTransferClient, locator, CancellationToken.None));
                _transformLogger.WriteLine(LogLevel.Information, String.Format("Added {0} to upload queue", assetFile.Name));
            }

            //Register progress callback:
            blobTransferClient.TransferProgressChanged +=
                new EventHandler<BlobTransferProgressChangedEventArgs>(blobTransferClient_TransferProgressChanged);

            //Wait for upload completion
            _transformLogger.WriteLine(LogLevel.Information, "Start waiting for upload");
            Task.WaitAll(uploadTasks.ToArray());
            _transformLogger.WriteLine(LogLevel.Information, "Done waiting for upload");

            //Clean up upload objects:
            if (locator != null) locator.Delete();
            if (accessPolicy != null) accessPolicy.Delete();
            blobTransferClient.TransferProgressChanged -= blobTransferClient_TransferProgressChanged;

            _transformLogger.WriteLine(LogLevel.Information, "Finished uploads.");
        }

        private int _previousProgressPercentage;
        void blobTransferClient_TransferProgressChanged(object sender, BlobTransferProgressChangedEventArgs e)
        {
            if (e.ProgressPercentage > 4) // Avoid startup jitter, as the upload tasks are added.
            {
                //Avoid spamming the system by only incrementing if higher than 5% change.
                if (e.ProgressPercentage - _previousProgressPercentage > 5)
                {
                    _previousProgressPercentage = e.ProgressPercentage;
                    int percentComplete = (int)(ProgressPercentageAttributedToUploading * (double)e.ProgressPercentage) + ProgressValuePriorToUploading;
                    _transformTaskStatus.UpdateStatus(percentComplete, JobStatus.Running, null);
                }
            }
        }



        /// <summary>
        /// Gets a VOD Locator for the Asset.  Sets the AssetMetadata.PrimaryFileUri.
        /// </summary>
        /// <see cref="http://msdn.microsoft.com/en-us/library/jj125563.aspx"/>
        private void CreateLocator()
        {
            //
            // Create a Locator for streaming
            //
            if (_accessPolicyDurationInDays > 0.0)
            {
                IAsset targetAsset = _context.Assets.Where(a => a.Id == _AssetMetadata.AssetId).FirstOrDefault();
                if (targetAsset == null)
                    throw new ApplicationException("Could not find the Asset by Asset.Id=" + _AssetMetadata.AssetId);

                var accessPolicyTimeout = TimeSpan.FromDays(_accessPolicyDurationInDays);
                IAccessPolicy readPolicy = _context.AccessPolicies.Create("Read Policy " + targetAsset.Id, accessPolicyTimeout, AccessPermissions.Read);
                var startTime = DateTime.UtcNow.AddMinutes(_locatorStartTimeInMinutes);

                var locatorId = "";
                var locatorPath = "";
                if (string.IsNullOrEmpty(_AssetMetadata.LocatorId))
                {
                    //Create a new locator, with a unique id:
                    ILocator locator = _context.Locators.CreateLocator(LocatorType.OnDemandOrigin, targetAsset,
                                                                       readPolicy, startTime);
                    locatorId = locator.Id;
                    locatorPath = locator.Path;
                }
                else
                {
                    //Create a locator with the REST api, this allows us to reuse the LocatorId.

                    //Get the Bearer Token required for all Windows Azure Media Services calls:
                    var acsBearerToken = RestHelper.GetAcsBearerToken(_wamsAccountName, _wamsAccountKey, _scope, _acsBaseAddress);
                    if (string.IsNullOrEmpty(acsBearerToken))
                        throw new ApplicationException("Could not get an Access Control Service Bearer Token.");

                    //Create the locator:
                    string redirectedApiServerUri;
                    var xmlResponse = RestHelper.CreateLocator(_apiServer, out redirectedApiServerUri, acsBearerToken,
                                                             targetAsset.Id, readPolicy.Id, (int)LocatorType.OnDemandOrigin,
                                                             startTime, _AssetMetadata.LocatorId);

                    //Use the response information:
                    if (xmlResponse == null)
                        throw new ApplicationException(String.Format("Could not create a locator using the LocatorId, {0}, found in the json file.", _AssetMetadata.LocatorId));
                    locatorId = xmlResponse.GetElementsByTagName("Id")[0].InnerText;
                    locatorPath = xmlResponse.GetElementsByTagName("Path")[0].InnerText;
                }

                _AssetMetadata.LocatorId = locatorId;
                _transformLogger.WriteLine(LogLevel.Information, "LocatorId= " + _AssetMetadata.LocatorId);

                _AssetMetadata.PrimaryFileUri = locatorPath + _AssetMetadata.PrimaryFile;
                _transformLogger.WriteLine(LogLevel.Information, "PrimaryFileUri= " + _AssetMetadata.PrimaryFileUri);
            }
        }



        /// <summary>
        /// Serialize _AssetMetadata to json and write a file in the output directory.
        /// </summary>
        private void SaveAssetMetadataAsJson()
        {
            try
            {
                //Bug: When this task is chained with itself, if you don't set output and input folders, the second task to run will overwrite the .json file.
                string outputFile = Path.Combine(Environment.CurrentDirectory, _transformMetadata.OutputFolder, Path.GetFileNameWithoutExtension(_AssetMetadata.PrimaryFile) + ".json");
                StreamWriter sr = File.CreateText(outputFile);
                sr.Write(_AssetMetadata.ToJson());
                sr.Close();
            }
            catch (Exception e)
            {
                _transformLogger.WriteLine(LogLevel.Warning, String.Format("Unable to save Asset metadata to .json file.  Exception:{0}", e.Message));
            }
        }



        /// <summary>
        /// Copies all the input files to the output folder.  This enables task chaining.
        /// </summary>
        /// <param name="inputDirectoryFiles"></param>
        private void CopyToOutputDirectory(List<FileInfo> inputDirectoryFiles)
        {
            string inputFolder = Path.Combine(Environment.CurrentDirectory, _transformMetadata.InputFolder);
            string outputFolder = Path.Combine(Environment.CurrentDirectory, _transformMetadata.OutputFolder);
            if (String.Compare(inputFolder, outputFolder, StringComparison.OrdinalIgnoreCase) != 0)
            {
                foreach (var fileInfo in inputDirectoryFiles)
                {
                    fileInfo.MoveTo(Path.Combine(outputFolder, fileInfo.Name));
                }
            }
        }

        #endregion //Runtime functions

        #region Helper functions

        /// <summary>
        /// Query for a fresh reference to an asset.  Use this during threaded operations or to avoid potential concurrency issues.
        /// </summary>
        /// <param name="assetId"></param>
        /// <returns></returns>
        public IAsset GetAssetById(string assetId)
        {
            // Use a Linq select query to get an updated reference by Id. 
            var asset =
                from a in _context.Assets
                where a.Id == assetId
                select a;

            IAsset theAsset = asset.FirstOrDefault();

            if (theAsset != null)
                return theAsset;
            else
                _transformLogger.WriteLine(LogLevel.Warning, String.Format("Unable to GetAssetById. assetId:{0}", assetId));

            return null;
        }

        /// <summary>
        /// Provisional. Query for a an asset by AlternateId.  Use this to get your asset and do something more with it directly from your CMS system.
        /// </summary>
        /// <param name="alternateId"></param>
        /// <returns></returns> 
        public IAsset GetAssetByAlternateId(string alternateId)
        {
            // Use a Linq select query to get an updated reference by Id. 
            var asset =
                from a in _context.Assets
                where a.AlternateId == alternateId
                select a;

            IAsset theAsset = asset.FirstOrDefault();  //If asset.Count() > 1, check how you are creating AlternateIds !

            if (theAsset != null)
                return theAsset;
            else
                _transformLogger.WriteLine(LogLevel.Warning, String.Format("Unable to GetAssetByAlternateId. AlternateId:{0}", alternateId));

            return null;
        }

        #endregion //Helper functions

        /// <summary>
        /// Helper class used to hold metadata as the task progresses.  Includes serialization it to json.
        /// </summary>
        [DataContract]
        public class AssetMetadata
        {
            [DataMember]
            public string AssetId;

            [DataMember]
            public string PrimaryFileUri;

            [DataMember]
            public string PrimaryFile;

            [DataMember]
            public string AlternateId;

            [DataMember]
            public string LocatorId;

            /// <summary>
            /// Converts the object to json.
            /// </summary>
            /// <returns>Properly escaped json string of the object.</returns>
            public string ToJson()
            {
                string jsonString = String.Empty;
                MemoryStream ms = null;
                try
                {
                    System.Runtime.Serialization.Json.DataContractJsonSerializer serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(AssetMetadata));
                    ms = new System.IO.MemoryStream();
                    serializer.WriteObject(ms, this);
                    jsonString = Encoding.UTF8.GetString(ms.ToArray());
                }
                catch (Exception e)
                {
                    //Do nothing, handle the null elsewhere.
                    System.Diagnostics.Debug.WriteLine(e.Message);
                }
                finally
                {
                    if (ms != null)
                    {
                        ms.Close();
                    }
                }
                return jsonString;
            }

            /// <summary>
            /// Provisional.  You can use this to parse the file that was saved in the output folder.
            /// </summary>
            /// <param name="jsonString"></param>
            /// <returns></returns>
            public static AssetMetadata FromJson(string jsonString)
            {
                AssetMetadata AssetMeta = new AssetMetadata();
                if (!String.IsNullOrEmpty(jsonString))
                {
                    MemoryStream ms = null;
                    try
                    {
                        ms = new MemoryStream(Encoding.UTF8.GetBytes(jsonString));
                        System.Runtime.Serialization.Json.DataContractJsonSerializer serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(AssetMetadata));
                        AssetMeta = serializer.ReadObject(ms) as AssetMetadata;
                    }
                    catch (Exception e)
                    {
                        //Do nothing, handle the null elsewhere.
                        System.Diagnostics.Debug.WriteLine(e.Message);
                    }
                    finally
                    {
                        if (ms != null)
                        {
                            ms.Close();
                        }
                    }
                }
                return AssetMeta;
            }
        }


    }
}
